#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
图像文件处理器模块，提供图像文件的读取、保存、转换、调整大小等功能
"""

import os
import io
import base64
from pathlib import Path
from typing import Union, List, Dict, Optional, Tuple, Any, BinaryIO

from ..exception.exceptions import FileException
from ..logger.logger import get_logger
from ..exception.decorators import exception_handler

logger = get_logger(__name__)


class ImageFileHandler:
    """
    图像文件处理器，提供图像文件的读取、保存、转换、调整大小等功能
    
    该类需要安装PIL库：pip install Pillow
    
    Attributes:
        None
    """
    
    def __init__(self):
        """
        初始化图像文件处理器
        
        Raises:
            ImportError: 如果未安装PIL库则抛出
        """
        try:
            from PIL import Image, ImageOps, ImageEnhance, ImageFilter
            self.Image = Image
            self.ImageOps = ImageOps
            self.ImageEnhance = ImageEnhance
            self.ImageFilter = ImageFilter
            logger.info("图像文件处理器初始化成功")
        except ImportError:
            raise ImportError("使用ImageFileHandler需要安装PIL库: pip install Pillow")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def open_image(self, image_path: Union[str, Path]) -> Any:
        """
        打开图像文件
        
        Args:
            image_path: 图像文件路径
            
        Returns:
            PIL.Image.Image: 图像对象
            
        Raises:
            FileException: 文件不存在或打开失败时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> image = handler.open_image('example.jpg')
            >>> print(f"图像大小: {image.size}")
        """
        image_path = Path(image_path)
        
        if not image_path.exists():
            raise FileException(f"图像文件不存在: {image_path}")
        
        try:
            image = self.Image.open(image_path)
            logger.debug(f"图像文件打开成功: {image_path}")
            return image
        except Exception as e:
            raise FileException(f"打开图像文件失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def save_image(self, image: Any, output_path: Union[str, Path], 
                  format: str = None, quality: int = 95, **kwargs) -> None:
        """
        保存图像到文件
        
        Args:
            image: PIL图像对象
            output_path: 输出文件路径
            format: 图像格式，默认为None（根据文件扩展名自动确定）
            quality: 图像质量（1-100），默认为95，仅对JPEG等有损格式有效
            **kwargs: 传递给PIL.Image.save()的其他参数
            
        Raises:
            FileException: 保存失败时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> image = handler.open_image('input.jpg')
            >>> handler.save_image(image, 'output.png', format='PNG')
        """
        output_path = Path(output_path)
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            # 如果未指定格式，根据文件扩展名确定
            if format is None:
                format = output_path.suffix.lstrip('.').upper()
                if not format:
                    format = 'JPEG'  # 默认格式
            
            # 保存图像
            image.save(output_path, format=format, quality=quality, **kwargs)
            logger.info(f"图像保存成功: {output_path}")
        except Exception as e:
            raise FileException(f"保存图像失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def convert_format(self, image_path: Union[str, Path], output_path: Union[str, Path], 
                       format: str = None, quality: int = 95, **kwargs) -> None:
        """
        转换图像格式
        
        Args:
            image_path: 输入图像文件路径
            output_path: 输出图像文件路径
            format: 输出图像格式，默认为None（根据输出文件扩展名自动确定）
            quality: 图像质量（1-100），默认为95，仅对JPEG等有损格式有效
            **kwargs: 传递给PIL.Image.save()的其他参数
            
        Raises:
            FileException: 文件不存在或转换失败时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> handler.convert_format('input.jpg', 'output.png')
            >>> handler.convert_format('input.jpg', 'output.webp', quality=80)
        """
        # 打开图像
        image = self.open_image(image_path)
        
        # 如果图像模式是RGBA，但目标格式不支持透明度，则转换为RGB
        if image.mode == 'RGBA' and format and format.upper() in ['JPEG', 'JPG']:
            image = image.convert('RGB')
        
        # 保存为新格式
        self.save_image(image, output_path, format=format, quality=quality, **kwargs)
        logger.info(f"图像格式转换成功: {image_path} -> {output_path}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def resize_image(self, image_path: Union[str, Path], output_path: Union[str, Path], 
                     width: int = None, height: int = None, 
                     keep_aspect_ratio: bool = True, resample: int = None) -> None:
        """
        调整图像大小
        
        Args:
            image_path: 输入图像文件路径
            output_path: 输出图像文件路径
            width: 目标宽度，默认为None
            height: 目标高度，默认为None
            keep_aspect_ratio: 是否保持宽高比，默认为True
            resample: 重采样方法，默认为None（使用PIL.Image.LANCZOS）
            
        Raises:
            FileException: 文件不存在或调整大小失败时抛出
            ValueError: 宽度和高度都为None时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> handler.resize_image('input.jpg', 'output.jpg', width=800)
            >>> handler.resize_image('input.jpg', 'output.jpg', width=800, height=600, keep_aspect_ratio=False)
        """
        if width is None and height is None:
            raise ValueError("宽度和高度不能同时为None")
        
        # 打开图像
        image = self.open_image(image_path)
        
        # 设置重采样方法
        if resample is None:
            resample = self.Image.LANCZOS if hasattr(self.Image, 'LANCZOS') else self.Image.BICUBIC
        
        try:
            # 获取原始尺寸
            orig_width, orig_height = image.size
            
            # 计算新尺寸
            if keep_aspect_ratio:
                if width is None:
                    # 根据高度等比例计算宽度
                    width = int(orig_width * (height / orig_height))
                elif height is None:
                    # 根据宽度等比例计算高度
                    height = int(orig_height * (width / orig_width))
                else:
                    # 宽度和高度都指定了，但要保持宽高比，以较小的缩放比例为准
                    width_ratio = width / orig_width
                    height_ratio = height / orig_height
                    ratio = min(width_ratio, height_ratio)
                    width = int(orig_width * ratio)
                    height = int(orig_height * ratio)
            else:
                # 不保持宽高比，如果某个维度未指定，则使用原始尺寸
                if width is None:
                    width = orig_width
                if height is None:
                    height = orig_height
            
            # 调整大小
            resized_image = image.resize((width, height), resample=resample)
            
            # 保存图像
            self.save_image(resized_image, output_path)
            logger.info(f"图像大小调整成功: {image_path} -> {output_path} ({width}x{height})")
        except Exception as e:
            raise FileException(f"调整图像大小失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def crop_image(self, image_path: Union[str, Path], output_path: Union[str, Path], 
                  left: int, top: int, right: int, bottom: int) -> None:
        """
        裁剪图像
        
        Args:
            image_path: 输入图像文件路径
            output_path: 输出图像文件路径
            left: 左边界坐标
            top: 上边界坐标
            right: 右边界坐标
            bottom: 下边界坐标
            
        Raises:
            FileException: 文件不存在或裁剪失败时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> handler.crop_image('input.jpg', 'output.jpg', 100, 100, 500, 400)
        """
        # 打开图像
        image = self.open_image(image_path)
        
        try:
            # 裁剪图像
            cropped_image = image.crop((left, top, right, bottom))
            
            # 保存图像
            self.save_image(cropped_image, output_path)
            logger.info(f"图像裁剪成功: {image_path} -> {output_path}")
        except Exception as e:
            raise FileException(f"裁剪图像失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def rotate_image(self, image_path: Union[str, Path], output_path: Union[str, Path], 
                     angle: float, expand: bool = False, fillcolor: Any = None) -> None:
        """
        旋转图像
        
        Args:
            image_path: 输入图像文件路径
            output_path: 输出图像文件路径
            angle: 旋转角度（度数，顺时针方向）
            expand: 是否扩展画布以适应旋转后的图像，默认为False
            fillcolor: 填充颜色，默认为None
            
        Raises:
            FileException: 文件不存在或旋转失败时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> handler.rotate_image('input.jpg', 'output.jpg', 90)
            >>> handler.rotate_image('input.jpg', 'output.jpg', 45, expand=True, fillcolor='white')
        """
        # 打开图像
        image = self.open_image(image_path)
        
        try:
            # 旋转图像
            rotated_image = image.rotate(angle, expand=expand, fillcolor=fillcolor)
            
            # 保存图像
            self.save_image(rotated_image, output_path)
            logger.info(f"图像旋转成功: {image_path} -> {output_path} (角度: {angle}°)")
        except Exception as e:
            raise FileException(f"旋转图像失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def flip_image(self, image_path: Union[str, Path], output_path: Union[str, Path], 
                   horizontal: bool = True) -> None:
        """
        翻转图像
        
        Args:
            image_path: 输入图像文件路径
            output_path: 输出图像文件路径
            horizontal: 是否水平翻转，默认为True；如果为False则垂直翻转
            
        Raises:
            FileException: 文件不存在或翻转失败时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> handler.flip_image('input.jpg', 'output.jpg')  # 水平翻转
            >>> handler.flip_image('input.jpg', 'output.jpg', horizontal=False)  # 垂直翻转
        """
        # 打开图像
        image = self.open_image(image_path)
        
        try:
            # 翻转图像
            if horizontal:
                flipped_image = self.ImageOps.mirror(image)
                flip_type = "水平"
            else:
                flipped_image = self.ImageOps.flip(image)
                flip_type = "垂直"
            
            # 保存图像
            self.save_image(flipped_image, output_path)
            logger.info(f"图像{flip_type}翻转成功: {image_path} -> {output_path}")
        except Exception as e:
            raise FileException(f"翻转图像失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def adjust_brightness(self, image_path: Union[str, Path], output_path: Union[str, Path], 
                          factor: float) -> None:
        """
        调整图像亮度
        
        Args:
            image_path: 输入图像文件路径
            output_path: 输出图像文件路径
            factor: 亮度调整因子，大于1增加亮度，小于1降低亮度
            
        Raises:
            FileException: 文件不存在或调整失败时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> handler.adjust_brightness('input.jpg', 'output.jpg', 1.5)  # 增加亮度
            >>> handler.adjust_brightness('input.jpg', 'output.jpg', 0.7)  # 降低亮度
        """
        # 打开图像
        image = self.open_image(image_path)
        
        try:
            # 调整亮度
            enhancer = self.ImageEnhance.Brightness(image)
            adjusted_image = enhancer.enhance(factor)
            
            # 保存图像
            self.save_image(adjusted_image, output_path)
            logger.info(f"图像亮度调整成功: {image_path} -> {output_path} (因子: {factor})")
        except Exception as e:
            raise FileException(f"调整图像亮度失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def adjust_contrast(self, image_path: Union[str, Path], output_path: Union[str, Path], 
                        factor: float) -> None:
        """
        调整图像对比度
        
        Args:
            image_path: 输入图像文件路径
            output_path: 输出图像文件路径
            factor: 对比度调整因子，大于1增加对比度，小于1降低对比度
            
        Raises:
            FileException: 文件不存在或调整失败时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> handler.adjust_contrast('input.jpg', 'output.jpg', 1.5)  # 增加对比度
            >>> handler.adjust_contrast('input.jpg', 'output.jpg', 0.7)  # 降低对比度
        """
        # 打开图像
        image = self.open_image(image_path)
        
        try:
            # 调整对比度
            enhancer = self.ImageEnhance.Contrast(image)
            adjusted_image = enhancer.enhance(factor)
            
            # 保存图像
            self.save_image(adjusted_image, output_path)
            logger.info(f"图像对比度调整成功: {image_path} -> {output_path} (因子: {factor})")
        except Exception as e:
            raise FileException(f"调整图像对比度失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def apply_filter(self, image_path: Union[str, Path], output_path: Union[str, Path], 
                     filter_type: str) -> None:
        """
        应用图像滤镜
        
        Args:
            image_path: 输入图像文件路径
            output_path: 输出图像文件路径
            filter_type: 滤镜类型，支持'blur'、'contour'、'detail'、'edge_enhance'、
                        'edge_enhance_more'、'emboss'、'find_edges'、'sharpen'、
                        'smooth'、'smooth_more'
            
        Raises:
            FileException: 文件不存在或应用滤镜失败时抛出
            ValueError: 不支持的滤镜类型时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> handler.apply_filter('input.jpg', 'output.jpg', 'blur')
            >>> handler.apply_filter('input.jpg', 'output.jpg', 'sharpen')
        """
        # 打开图像
        image = self.open_image(image_path)
        
        # 支持的滤镜类型
        filters = {
            'blur': self.ImageFilter.BLUR,
            'contour': self.ImageFilter.CONTOUR,
            'detail': self.ImageFilter.DETAIL,
            'edge_enhance': self.ImageFilter.EDGE_ENHANCE,
            'edge_enhance_more': self.ImageFilter.EDGE_ENHANCE_MORE,
            'emboss': self.ImageFilter.EMBOSS,
            'find_edges': self.ImageFilter.FIND_EDGES,
            'sharpen': self.ImageFilter.SHARPEN,
            'smooth': self.ImageFilter.SMOOTH,
            'smooth_more': self.ImageFilter.SMOOTH_MORE
        }
        
        if filter_type.lower() not in filters:
            raise ValueError(f"不支持的滤镜类型: {filter_type}")
        
        try:
            # 应用滤镜
            filtered_image = image.filter(filters[filter_type.lower()])
            
            # 保存图像
            self.save_image(filtered_image, output_path)
            logger.info(f"图像滤镜应用成功: {image_path} -> {output_path} (滤镜: {filter_type})")
        except Exception as e:
            raise FileException(f"应用图像滤镜失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def convert_to_grayscale(self, image_path: Union[str, Path], output_path: Union[str, Path]) -> None:
        """
        将图像转换为灰度图
        
        Args:
            image_path: 输入图像文件路径
            output_path: 输出图像文件路径
            
        Raises:
            FileException: 文件不存在或转换失败时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> handler.convert_to_grayscale('input.jpg', 'output.jpg')
        """
        # 打开图像
        image = self.open_image(image_path)
        
        try:
            # 转换为灰度图
            grayscale_image = image.convert('L')
            
            # 保存图像
            self.save_image(grayscale_image, output_path)
            logger.info(f"图像转换为灰度图成功: {image_path} -> {output_path}")
        except Exception as e:
            raise FileException(f"转换图像为灰度图失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def add_watermark(self, image_path: Union[str, Path], output_path: Union[str, Path], 
                      watermark_text: str, position: Tuple[int, int] = None, 
                      font_size: int = 20, font_color: Any = 'white', opacity: float = 0.5) -> None:
        """
        添加文字水印
        
        Args:
            image_path: 输入图像文件路径
            output_path: 输出图像文件路径
            watermark_text: 水印文字
            position: 水印位置坐标(x, y)，默认为None（居中）
            font_size: 字体大小，默认为20
            font_color: 字体颜色，默认为'white'
            opacity: 水印不透明度（0-1），默认为0.5
            
        Raises:
            FileException: 文件不存在或添加水印失败时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> handler.add_watermark('input.jpg', 'output.jpg', 'Copyright 2023')
            >>> handler.add_watermark('input.jpg', 'output.jpg', 'CONFIDENTIAL', 
            ...                      position=(100, 100), font_size=30, font_color='red')
        """
        # 打开图像
        image = self.open_image(image_path)
        
        try:
            from PIL import ImageDraw, ImageFont
            
            # 创建一个透明的图层用于绘制水印
            watermark = self.Image.new('RGBA', image.size, (0, 0, 0, 0))
            draw = ImageDraw.Draw(watermark)
            
            # 尝试加载默认字体，如果失败则使用默认字体
            try:
                font = ImageFont.truetype("arial.ttf", font_size)
            except IOError:
                font = ImageFont.load_default()
            
            # 计算文本大小
            text_width, text_height = draw.textsize(watermark_text, font=font) if hasattr(draw, 'textsize') else (font_size * len(watermark_text), font_size * 1.5)
            
            # 如果未指定位置，则居中放置
            if position is None:
                position = ((image.width - text_width) // 2, (image.height - text_height) // 2)
            
            # 绘制水印文字
            draw.text(position, watermark_text, font=font, fill=font_color)
            
            # 设置水印透明度
            watermark = watermark.convert('RGBA')
            alpha = int(opacity * 255)
            watermark.putalpha(alpha)
            
            # 将水印合并到原图
            if image.mode != 'RGBA':
                image = image.convert('RGBA')
            
            watermarked_image = self.Image.alpha_composite(image, watermark)
            
            # 保存图像
            self.save_image(watermarked_image, output_path)
            logger.info(f"图像添加水印成功: {image_path} -> {output_path}")
        except Exception as e:
            raise FileException(f"添加图像水印失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def get_image_info(self, image_path: Union[str, Path]) -> Dict[str, Any]:
        """
        获取图像信息
        
        Args:
            image_path: 图像文件路径
            
        Returns:
            Dict[str, Any]: 包含图像信息的字典，包括尺寸、模式、格式、EXIF数据等
            
        Raises:
            FileException: 文件不存在或获取信息失败时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> info = handler.get_image_info('example.jpg')
            >>> print(f"图像尺寸: {info['size']}")
            >>> print(f"图像格式: {info['format']}")
        """
        # 打开图像
        image = self.open_image(image_path)
        
        try:
            # 获取基本信息
            info = {
                'size': image.size,
                'width': image.width,
                'height': image.height,
                'mode': image.mode,
                'format': image.format,
                'palette': bool(image.palette) if hasattr(image, 'palette') else None,
            }
            
            # 获取EXIF数据（如果有）
            exif_data = {}
            if hasattr(image, '_getexif') and image._getexif():
                exif = image._getexif()
                if exif:
                    from PIL.ExifTags import TAGS
                    for tag_id, value in exif.items():
                        tag = TAGS.get(tag_id, tag_id)
                        exif_data[tag] = value
            
            info['exif'] = exif_data
            
            # 获取DPI信息
            if 'dpi' in image.info:
                info['dpi'] = image.info['dpi']
            
            return info
        except Exception as e:
            raise FileException(f"获取图像信息失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def create_thumbnail(self, image_path: Union[str, Path], output_path: Union[str, Path], 
                         size: Tuple[int, int]) -> None:
        """
        创建缩略图
        
        Args:
            image_path: 输入图像文件路径
            output_path: 输出图像文件路径
            size: 缩略图尺寸(宽, 高)，图像将被缩放以适应该尺寸，但不会被拉伸
            
        Raises:
            FileException: 文件不存在或创建缩略图失败时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> handler.create_thumbnail('input.jpg', 'thumbnail.jpg', (128, 128))
        """
        # 打开图像
        image = self.open_image(image_path)
        
        try:
            # 创建副本以避免修改原图
            thumbnail = image.copy()
            
            # 创建缩略图
            thumbnail.thumbnail(size, self.Image.LANCZOS if hasattr(self.Image, 'LANCZOS') else self.Image.BICUBIC)
            
            # 保存图像
            self.save_image(thumbnail, output_path)
            logger.info(f"缩略图创建成功: {image_path} -> {output_path} ({thumbnail.width}x{thumbnail.height})")
        except Exception as e:
            raise FileException(f"创建缩略图失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def image_to_base64(self, image_path: Union[str, Path], format: str = None) -> str:
        """
        将图像转换为Base64编码字符串
        
        Args:
            image_path: 图像文件路径
            format: 图像格式，默认为None（使用原始格式）
            
        Returns:
            str: Base64编码的图像字符串
            
        Raises:
            FileException: 文件不存在或转换失败时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> base64_str = handler.image_to_base64('example.jpg')
            >>> print(f"Base64编码: {base64_str[:50]}...")
        """
        # 打开图像
        image = self.open_image(image_path)
        
        try:
            # 如果未指定格式，使用原始格式
            if format is None:
                format = image.format if image.format else 'JPEG'
            
            # 将图像保存到内存缓冲区
            buffer = io.BytesIO()
            image.save(buffer, format=format)
            buffer.seek(0)
            
            # 转换为Base64
            base64_str = base64.b64encode(buffer.getvalue()).decode('utf-8')
            logger.debug(f"图像转换为Base64成功: {image_path}")
            return base64_str
        except Exception as e:
            raise FileException(f"将图像转换为Base64失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def base64_to_image(self, base64_str: str, output_path: Union[str, Path], format: str = 'JPEG') -> None:
        """
        将Base64编码字符串转换为图像
        
        Args:
            base64_str: Base64编码的图像字符串
            output_path: 输出图像文件路径
            format: 图像格式，默认为'JPEG'
            
        Raises:
            FileException: 转换失败时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> handler.base64_to_image(base64_str, 'output.jpg')
        """
        try:
            # 解码Base64字符串
            image_data = base64.b64decode(base64_str)
            
            # 从二进制数据创建图像
            image = self.Image.open(io.BytesIO(image_data))
            
            # 保存图像
            self.save_image(image, output_path, format=format)
            logger.info(f"Base64转换为图像成功: {output_path}")
        except Exception as e:
            raise FileException(f"将Base64转换为图像失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def combine_images(self, image_paths: List[Union[str, Path]], output_path: Union[str, Path], 
                       direction: str = 'horizontal', spacing: int = 0) -> None:
        """
        合并多个图像
        
        Args:
            image_paths: 输入图像文件路径列表
            output_path: 输出图像文件路径
            direction: 合并方向，'horizontal'（水平）或'vertical'（垂直），默认为'horizontal'
            spacing: 图像之间的间距（像素），默认为0
            
        Raises:
            FileException: 文件不存在或合并失败时抛出
            ValueError: 图像路径列表为空或方向无效时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> handler.combine_images(['img1.jpg', 'img2.jpg', 'img3.jpg'], 'combined.jpg')
            >>> handler.combine_images(['img1.jpg', 'img2.jpg'], 'combined.jpg', direction='vertical', spacing=10)
        """
        if not image_paths:
            raise ValueError("图像路径列表不能为空")
        
        if direction not in ['horizontal', 'vertical']:
            raise ValueError("方向必须是'horizontal'或'vertical'")
        
        # 打开所有图像
        images = [self.open_image(path) for path in image_paths]
        
        try:
            # 确定合并后的尺寸
            if direction == 'horizontal':
                # 水平合并：宽度是所有图像宽度之和加上间距，高度是最高图像的高度
                width = sum(img.width for img in images) + spacing * (len(images) - 1)
                height = max(img.height for img in images)
            else:  # vertical
                # 垂直合并：宽度是最宽图像的宽度，高度是所有图像高度之和加上间距
                width = max(img.width for img in images)
                height = sum(img.height for img in images) + spacing * (len(images) - 1)
            
            # 创建新图像
            combined = self.Image.new('RGBA', (width, height), (0, 0, 0, 0))
            
            # 粘贴图像
            offset = 0
            for img in images:
                if img.mode != 'RGBA':
                    img = img.convert('RGBA')
                
                if direction == 'horizontal':
                    combined.paste(img, (offset, (height - img.height) // 2), img)
                    offset += img.width + spacing
                else:  # vertical
                    combined.paste(img, ((width - img.width) // 2, offset), img)
                    offset += img.height + spacing
            
            # 保存图像
            self.save_image(combined, output_path)
            logger.info(f"图像合并成功: {len(images)} 个图像 -> {output_path}")
        except Exception as e:
            raise FileException(f"合并图像失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def create_image_grid(self, image_paths: List[Union[str, Path]], output_path: Union[str, Path], 
                          grid_size: Tuple[int, int], cell_size: Tuple[int, int] = None, 
                          padding: int = 0, background_color: Any = 'white') -> None:
        """
        创建图像网格
        
        Args:
            image_paths: 输入图像文件路径列表
            output_path: 输出图像文件路径
            grid_size: 网格尺寸(列数, 行数)
            cell_size: 单元格尺寸(宽, 高)，默认为None（根据图像自动确定）
            padding: 单元格之间的间距（像素），默认为0
            background_color: 背景颜色，默认为'white'
            
        Raises:
            FileException: 文件不存在或创建网格失败时抛出
            ValueError: 图像路径列表为空或网格尺寸无效时抛出
        
        Examples:
            >>> handler = ImageFileHandler()
            >>> handler.create_image_grid(['img1.jpg', 'img2.jpg', 'img3.jpg', 'img4.jpg'], 
            ...                          'grid.jpg', (2, 2), padding=5)
        """
        if not image_paths:
            raise ValueError("图像路径列表不能为空")
        
        cols, rows = grid_size
        if cols <= 0 or rows <= 0:
            raise ValueError("网格尺寸必须大于0")
        
        # 打开所有图像
        images = [self.open_image(path) for path in image_paths]
        
        try:
            # 如果未指定单元格尺寸，根据图像自动确定
            if cell_size is None:
                cell_width = max(img.width for img in images)
                cell_height = max(img.height for img in images)
                cell_size = (cell_width, cell_height)
            else:
                cell_width, cell_height = cell_size
            
            # 计算网格尺寸
            grid_width = cols * cell_width + (cols - 1) * padding
            grid_height = rows * cell_height + (rows - 1) * padding
            
            # 创建网格图像
            grid = self.Image.new('RGBA', (grid_width, grid_height), background_color)
            
            # 放置图像
            for i, img in enumerate(images):
                if i >= cols * rows:  # 超出网格范围的图像将被忽略
                    break
                
                # 计算行列位置
                col = i % cols
                row = i // cols
                
                # 调整图像大小以适应单元格
                resized_img = img.copy()
                resized_img.thumbnail(cell_size, self.Image.LANCZOS if hasattr(self.Image, 'LANCZOS') else self.Image.BICUBIC)
                
                # 计算粘贴位置（居中）
                x = col * (cell_width + padding) + (cell_width - resized_img.width) // 2
                y = row * (cell_height + padding) + (cell_height - resized_img.height) // 2
                
                # 粘贴图像
                if resized_img.mode != 'RGBA':
                    resized_img = resized_img.convert('RGBA')
                
                grid.paste(resized_img, (x, y), resized_img)
            
            # 保存图像
            self.save_image(grid, output_path)
            logger.info(f"图像网格创建成功: {len(images)} 个图像 -> {output_path}")
        except Exception as e:
            raise FileException(f"创建图像网格失败: {e}")